<?php
/**
 * Created by PhpStorm.
 * User: qv
 * Date: 2018/7/27 0027
 * Time: 10:38
 */

namespace Database\base;


use Database\ActiveInterface;
use Database\Collection;
use Database\Connection;
use Database\DbBase;
use Database\Model;

abstract class ActiveRecord extends DbBase implements ActiveInterface, \ArrayAccess
{

    /** @var array */
    protected $_attributes = [];

    /** @var array */
    protected $_oldAttributes = [];

    /** @var array */
    protected $_relate = [];

    /** @var null|string */
    protected $primary = NULL;

    /**
     * @var bool
     */
    protected $isNewExample = TRUE;

    /**
     * @var array
     */
    protected $column = [];

    /** @var Connection */
    public $db = NULL;

    /**
     * @return bool
     */
    public function getIsCreate()
    {
        return $this->isNewExample;
    }

    /**
     * @return $this
     */
    public function setIsCreate($bool = FALSE)
    {
        $this->isNewExample = $bool;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasPrimary()
    {
        return $this->primary !== NULL;
    }

    /**
     * @return null|string
     */
    public function getPrimary()
    {
        return $this->hasPrimary() ? $this->primary : NULL;
    }

    /**
     * @param $condition
     *
     * @return $this
     * @throws \Exception
     */
    public static function findOne($condition, $db = NULL)
    {
        if (empty($condition) || !is_numeric($condition)) {
            throw new DbException("Not primary value by find one.");
        }
        return static::find()->useDb($db)->where($condition)->one();
    }

    /**
     * @return mixed|Model
     * @throws \Exception
     */
    public static function find()
    {
        static $model;
        if (!$model instanceof Model) {
            $model = new Model(get_called_class());
        }
        return $model;
    }

    /**
     * @return bool
     * @throws DbException
     * @throws \Exception
     */
    public function delete()
    {
        if ($this->hasPrimary()) {
            $where = $this->getPrimary() . '=' . $this->getPrValue();
        } else {
            if (empty($this->_attributes)) {
                return FALSE;
            }
            $where = $this->_attributes;
        }
        if (empty($where)) {
            return FALSE;
        }
        if (static::deleteAll($where)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

    /**
     * @param       $condition
     * @param array $attributes
     *
     * @return bool
     * @throws \Exception
     */
    public static function deleteAll($condition, $attributes = [])
    {
        if (empty($condition)) {
            return static::find()->deleteAll();
        }
        $model = static::find()->where($condition);
        if (!empty($attributes)) {
            $model->bindParams($attributes);
        }
        return $model->deleteAll();
    }

    /**
     * @param       $condition
     * @param array $attributes
     *
     * @return array|mixed|null|\Yoc\Db\Collection
     * @throws \Exception
     */
    public static function findAll($condition, $attributes = [])
    {
        if (!empty($attributes)) {
            return static::find()->where($condition, $attributes)->all();
        } else {
            return static::find()->where($condition)->all();
        }
    }

    /**
     * @param array $data
     *
     * @return bool|mixed|\Yoc\Db\ActiveRecord
     * @throws \Exception
     */
    public function save($data = NULL)
    {
        Debug::trance('before save');
        $this->beforeSave();
        $flip = $this->getColumn();
        $this->_attributes = array_merge($flip, $this->_attributes);
        if ($this->getIsCreate()) {
            return $this->insertInternal($data);
        } else {
            return $this->updateInternal($data);
        }
    }

    /**
     * @return bool
     * @throws \Exception
     */
    public function beforeSave()
    {
        if (empty($this->rules())) {
            return TRUE;
        }
        $validate = $this->resolve($this->rules());
        if (!($validate instanceof Validate) || !$validate->validator()) {
            return $this->addError($validate->getError(), get_called_class());
        } else {
            return TRUE;
        }
    }

    /**
     * @return bool|BaseActiveRecord
     * @throws \Exception
     */
    private function insertInternal($db)
    {
        if (!$db) {
            $db = static::getDb();
        }

        list($attributes, $param) = $this->each($this->_attributes);
        if (empty($attributes) || empty($param)) {
            return FALSE;
        }

        list($sql, $param) = $db->getQueryBuilder()->insert($this->getTable(), $attributes, $param);

        Debug::trance('start save');

        $command = $db->createCommand($sql, $param);
        $lastId = $command->save(TRUE);

        Debug::trance('save over.');
        if ($lastId === FALSE) {
            return FALSE;
        }
        if (!empty($this->getPrimary()) && $lastId) {
            $param[$this->getPrimary()] = $lastId;
        }

        $this->after($param);

        return $this->populate($param);
    }


    /**
     * @param string $name
     * @return null
     * @throws \Exception
     */
    public function getAttribute(string $name)
    {
        if ($this->has($name)) {
            if (empty($this->_attributes)) {
                return NULL;
            }
            return $this->$name;
        } else {
            return NULL;
        }
    }

    /**
     * @param array $data
     * @return bool
     * @throws DbException
     * @throws \Exception
     */
    public function addAll(array $data)
    {
        if (empty($data)) {
            return $this->addError('Insert data empty.');
        }
        $first = current($data);
        $last = $data[count($data) - 1];
        if (!is_array($first) || !is_array($last)) {
            return $this->addError('Insert data format error.');
        }
        $db = static::getDb();

        $attributes = array_keys(current($data));

        list($sql, $param) = $db->getQueryBuilder()->all($this->getTable(), $attributes, $data);

        $command = $db->createCommand($sql, $param);
        $lastId = $command->save(FALSE);
        if (!$lastId) {
            return FALSE;
        } else {
            return TRUE;
        }
    }

    /**
     * @return bool|BaseActiveRecord
     * @throws \Exception
     */
    private function updateInternal($db)
    {
        $renew = $this->isRenew();
        if (!$renew) {
            return TRUE;
        }

        if (!$db) {
            $db = static::getDb();
        }
        list($attributes, $param) = $this->each($renew);
        if (empty($attributes) || empty($param)) {
            return FALSE;
        }

        $condition = [];
        if ($this->hasPrimary()) {
            $condition[] = $this->getPrimary() . '=\'' . $this->_oldAttributes[$this->getPrimary()] . '\'';
        } else {
            $oldAttributes = array_diff_assoc($renew, $this->_attributes);

            foreach ($oldAttributes as $key => $val) {
                if (in_array($key, $attributes) && $param[$key] == $val) {
                    continue;
                }
                $condition[] = $key . '=\'' . $val . '\'';
            }
        }
        list($sql, $param) = $db->getQueryBuilder()->update($this->getTable(), $attributes, $condition, $param);
        $command = $db->createCommand($sql, $param);
        $lastId = $command->save(FALSE);
        if (!$lastId) {
            return FALSE;
        }
        $this->after($param);
        return $this->populate($this->_attributes);
    }

    /**
     * @param $param
     * @throws DbException
     * @throws \ReflectionException
     */
    private function after($param)
    {
        $parent = class_parents($this);
        if (method_exists($parent, 'afterSave')) {
            $parent->afterSave($this->_attributes, $param);
            $this->afterSave($this->_attributes, $param);
        } else {
            $this->afterSave($this->_attributes, $param);
        }
    }

    /**
     * @return array
     */
    private function isRenew()
    {
        $_tmp = [];
        foreach ($this->_attributes as $key => $val) {
            if ($val == $this->_oldAttributes[$key]) {
                continue;
            }
            $_tmp[$key] = $val;
        }
        return $_tmp;
    }

    /**
     * @param $data
     *
     * @return array
     */
    private function each($data)
    {
        $attributes = [];
        foreach ($data as $key => $val) {
            if ($val === NULL) continue;
            $attributes[] = $key;
            $param[$key] = $val;
        }
        return [$attributes, $param];
    }

    /**
     * @param $rule
     *
     * @return Validate
     * @throws \ReflectionException
     */
    private function resolve($rule)
    {
        $validate = Validate::getInstance();
        foreach ($rule as $Key => $val) {
            $field = array_shift($val);
            if (empty($val) || !($rule = $this->createRule($val))) {
                continue;
            }
            $this->append($validate, $field, $rule);
        }
        return $validate->setParams($this->_attributes);
    }

    /**
     * @param Validate $validate
     * @param          $field
     * @param          $rule
     *
     * @return Validate
     * @throws \ReflectionException
     */
    private function append(Validate $validate, $field, $rule)
    {
        if (is_string($field)) {
            $validate->make([$field => $rule], $this->_attributes);
        } else {
            foreach ($field as $key => $_val) {
                $validate->make([$_val => $rule], $this->_attributes);
            }
        }
        return $validate;
    }

    /**
     * @param $rule
     *
     * @return null|string
     */
    private function createRule($rule)
    {
        $str = [];
        foreach ($rule as $key => $val) {
            if (is_numeric($key)) {
                $str[] = $val;
            } else {
                $str[] = $key . '=' . $val;
            }
        }
        if (!empty($str)) {
            return implode('|', $str);
        }
        return NULL;
    }

    /**
     * @param array $condition
     *
     * @throws \Exception
     * @return $this
     */
    protected function addCondition($condition = [])
    {
        $this->_query->whereAnd($condition);
        return $this;
    }

    /**
     * @param $name
     * @param $value
     */
    public function setRelate($name, $value)
    {
        $this->_relate[$name] = $value;
    }


    /**
     * @param $attribute
     *
     * @return bool
     * @throws DbException
     * @throws \ReflectionException
     */
    public function has($attribute)
    {
        if (isset($this->_attributes[$attribute])) {
            return TRUE;
        } else {
            return array_key_exists($attribute, $this->getColumn());
        }
    }

    /**
     * @return array
     * @throws DbException
     * @throws \ReflectionException
     * @throws \Exception
     */
    public function getColumn()
    {
        $db = static::getDb();
        $fileCache = \Yoc::$app->redis;
        $sql = $db->getQueryBuilder()->getColumn($this->getTable());
        $key = 'column_' . $this->getTable();
        $column = $fileCache->hGet($key, md5($sql));
        if ($column === NULL || !is_array($column = json_decode($column, TRUE))) {
            Debug::trance('get column');
            $column = $db->createCommand($sql)->all();
            if (empty($column)) {
                throw new DbException("The table " . $this->getTable() . " not exists.");
            }
            $fileCache->hSet($key, md5($sql), json_encode($column));
        }
        $_tmp = [];
        foreach ($column as $key => $val) {
            $_tmp[$val['Field']] = $val['Default'];
        }
        return $_tmp;
    }

    /**
     * @return string
     * @throws \Exception
     */
    public function getTable()
    {
        $tablePrefix = static::getDb()->tablePrefix;

        $table = static::tableName();

        if (strpos($table, $tablePrefix) === 0) {
            return $table;
        }

        if (empty($table)) {
            $class = preg_replace('/model\\\/', '', get_called_class());
            $table = lcfirst($class);
        }
        $table = trim($table, '{{%}}');
        if ($tablePrefix) {
            $table = $tablePrefix . $table;
        }
        return $table;
    }

    /**
     * @param $data
     * @throws
     * @return static
     */
    public function populate($data)
    {
        $this->_attributes = $data;
        $this->_oldAttributes = $data;
        return $this;
    }

    /**
     * @return null|string
     * @throws DbException
     * @throws \ReflectionException
     * @throws \Exception
     */
    public function getUniqueCachePrimary($id = NULL)
    {
        $table = static::getTable();
        if (!empty($id) && is_numeric($id)) {
            return 'model:' . $table . ':' . $id;
        }
        if ($this->hasPrimary()) {
            $primary = $this->getAttribute($this->getPrimary());
            if (!empty($primary)) {
                return 'model:' . $table . ':' . $primary;
            } else {
                return NULL;
            }
        } else {
            return NULL;
        }
    }

    /**
     * @return mixed
     * @throws \Exception
     */
    public function getPrValue()
    {
        return $this->getAttribute($this->getPrimary());
    }
}